package momo.app.comms;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;

import momo.app.config.AppConfig;
import momo.app.status.StatusManager;
import momo.app.structure.Message;
import momo.app.util.FileOutput;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


public class CommsHistory
{
	static private CommsHistory history;
	
	private Log log = LogFactory.getLog(CommsHistory.class);
	
	private Map<String, Integer> stateVector;
	private Map<String, Map<Integer, String>> msgAccepted;
	private Map<String, Map<Integer, String>> msgOutOfSeq;
	
	private String myId;
	
	private MsgTracker tracker;
	
	private CommsHistory()
	{
		stateVector = Collections.synchronizedMap(new HashMap<String, Integer>());
		msgAccepted = Collections.synchronizedMap(new HashMap<String, Map<Integer, String>>());
		msgOutOfSeq = Collections.synchronizedMap(new HashMap<String, Map<Integer,String>>());
		
//		myId = StatusManager.getInstance().getItself().getId();
		myId = AppConfig.getInstance().getMe().getId();
		addState(myId, 0);
		
		tracker = new MsgTracker();
	}
	
	static public CommsHistory getInstance()
	{
		if ( history == null )
			history = new CommsHistory();
		return history;
	}
	
	synchronized public boolean removeState(String id)
	{
		if ( log.isDebugEnabled() )
			log.debug("removeState of id: " + id);
		
		return stateVector.remove(id)==null?false:true;
	}
	
	synchronized public List<Message> rcvMsg(Message msg)
	{
		Vector<Message> toReturn = new Vector<Message>();
		
		String states = msg.getStates().trim();
		if ( states.equals("") )
		{
			if ( log.isDebugEnabled() )
				log.debug("No States found");
			return toReturn;
		}
		
		String sourceId = msg.getSourceId();
		int msgLocalSeqNo = msg.getLocalSeqNo();
		
		if ( log.isDebugEnabled() )
			log.debug("Rcved Message-> From: " + sourceId +", this msg state: " + msgLocalSeqNo);
		
		if ( sourceId.equals(myId) )
		{
			if ( log.isDebugEnabled() )
				log.debug("Own message found");
			
			return toReturn;
		}
		
//		pls UN-COMMENT Once testin is done
		
		if ( getState(sourceId) == 0 )
		{
			if ( log.isDebugEnabled() )
				log.debug("Accepting Msg from unknown source: " + sourceId +", msgLocalSeqNo: " + msgLocalSeqNo +", getState(sourceId): " + getState(sourceId));
			
			acceptMsg(msg);
			addState(sourceId, msgLocalSeqNo);
			toReturn.add(msg);
			toReturn.addAll(doUpdate());
			askUnknownStateMsg(sourceId, states);
			return toReturn;
		}
		
		int diff = msgLocalSeqNo - getState(sourceId);
//		ignore if source state of msg is found smaller than in my stateVector
		if ( diff <= 0 )
		{
			if ( log.isDebugEnabled() )
				log.debug("Message ignore");
			
			return toReturn;
		}else if ( diff == 1 )
		{
//			check if others(state) of msg are <= others(state) of my stateVector
			if ( stateInSynch(sourceId, states) )
			{
				if ( log.isDebugEnabled() ) {
					log.debug("Message in Synch with my state vector. sourceId: " + sourceId +", msgLocalSeqNo: " + msgLocalSeqNo +", getState(sourceId): " + getState(sourceId));
					log.debug("I m freeing the delay - rcvMsg");
				}
				Delayer.getInstance().freeDelay();
				
				acceptMsg(msg);
				addState(sourceId, msgLocalSeqNo);
				toReturn.add(msg);
				toReturn.addAll(doUpdate());
				askUnknownStateMsg(sourceId, states);
				return toReturn;
			}
		}
		
		if ( log.isDebugEnabled() )
			log.debug("Message lost synch with my state vector. sourceId: " + sourceId +", msgLocalSeqNo: " + msgLocalSeqNo +", getState(sourceId): " + getState(sourceId));
		
		Delayer.getInstance().delayMsgSent();
		
		addMsgToOutOfSeq(msg);
		askForMsg(sourceId, states);
		askUnknownStateMsg(sourceId, states);
		return toReturn;
	}
	
	synchronized private List<Message> doUpdate()
	{
		boolean hasNotLoopAll = true;
		Vector<Message> toRemove = new Vector<Message>();
		Vector<Message> toReturn = new Vector<Message>();
		
		while ( hasNotLoopAll )
		{
			Iterator<String> iterStr = msgOutOfSeq.keySet().iterator();
			while ( iterStr.hasNext() )
			{
				Map<Integer, String> map = msgOutOfSeq.get(iterStr.next());
				Iterator<Integer> iterInt = map.keySet().iterator();
				while ( iterInt.hasNext() )
				{
					Message msg = Message.createMessage(map.get(iterInt.next()));
					
					String states = msg.getStates().trim();
					String sourceId = msg.getSourceId();
					int msgLocalSeqNo = msg.getLocalSeqNo();
					
					int diff = msgLocalSeqNo - getState(sourceId);
					if ( diff <= 0 )
						toRemove.add(msg);
					else if ( diff == 1 && stateInSynch(sourceId, states) )
					{
						if ( log.isDebugEnabled() )
							log.debug("Found message in outOfSeq to update!! source: " + sourceId +", msgLocalSeqNo: " + msgLocalSeqNo +", getState(sourceId): " + getState(sourceId));
						
						acceptMsg(msg);
						addState(sourceId, msgLocalSeqNo);
						askUnknownStateMsg(sourceId, states);
						toRemove.add(msg);
						toReturn.add(msg);
					}
					
				}
			}
			
			if ( toRemove.size() == 0 )
				hasNotLoopAll = false;
			else
			{
				for(Message m: toRemove)	
					this.removeMsgFromOutOfSeq(m);
				toRemove.clear();
				System.gc();
			}
			
		}
		
		return toReturn;
	}
	
	synchronized private boolean stateInSynch(String sourceId, String states)
	{
		if ( log.isDebugEnabled() )
			log.debug("Checking other state ofthis  message");
		
		StringTokenizer statesToken = new StringTokenizer(states, "|");
		while ( statesToken.hasMoreTokens() )
		{
			StringTokenizer tokens = new StringTokenizer(statesToken.nextToken(), "~");
			String nodeId = tokens.nextToken();
			int msgLocalSeqNo = Integer.parseInt(tokens.nextToken());
			
			if ( !nodeId.equals(sourceId) )
			{
				int stateInSV = getState(nodeId);
				if ( stateInSV > 0 )
				{
					if ( msgLocalSeqNo > stateInSV )
					{
						if ( log.isDebugEnabled() )
							log.debug("State failed -> NodeId: " + nodeId +", current State " + stateInSV + ", state in msg: " + msgLocalSeqNo);
						
						return false;
					}
				}
			}
		}
		return true;
	}
	
	private void askForMsg(String sourceId, String states)
	{
		
		StringTokenizer statesToken = new StringTokenizer(states, "|");
		while ( statesToken.hasMoreTokens() )
		{
			StringTokenizer tokens = new StringTokenizer(statesToken.nextToken(), "~");
			String nodeId = tokens.nextToken();
			int msgLocalSeqNo = Integer.parseInt(tokens.nextToken());
			
			if ( !nodeId.equals(myId) )
			{
				int stateInSV = getState(nodeId);
				if ( stateInSV > 0 )
				{
					int diff = msgLocalSeqNo - stateInSV;
					
					if ( diff > 0 )
					{
						if ( log.isDebugEnabled() )
							log.debug("nodeId: " + nodeId +", msgLocalSeqNo: " + msgLocalSeqNo + ", getState(nodeId): " + stateInSV);
						
						StringBuffer buffer = new StringBuffer();
						int prevState = -1;
						boolean hasStartLink = true;
						
						for(int i=0; i<diff; i++)
						{
							int msgStateToAsk = (stateInSV + 1 + i);
							
							if ( log.isDebugEnabled() )
								log.debug("nodeId: " + nodeId + ", curStateToAsk: " + msgStateToAsk);
							
							if ( !isMsgInOutOfSeq(nodeId, msgStateToAsk) )
							{
//								add timer to lost msg
//								if ( tracker.addCheckerOnLostMsg(nodeId, msgStateToAsk) )
//								{
									if ( (msgStateToAsk-prevState) == 1 )
									{
										if (!hasStartLink)
										{
											hasStartLink = true;
											buffer.append("-");
										}
									}else
									{
										if ( !hasStartLink )
										{
											buffer.append(","+msgStateToAsk);
										}else
										{
											hasStartLink = false;
											buffer.append(msgStateToAsk);
										}
									}
									prevState = msgStateToAsk;
//								}//end of if ( tracker.addCheckerOnLostMsg(nodeId, msgStateToAsk) )
							}//end of if ( !isMsgInOutOfSeq(nodeId, msgStateToAsk) )
						}//end for
						
						if ( hasStartLink && prevState > 0 )
							buffer.append(prevState);
						
						if ( buffer.length() > 0 )
						{
							buffer.insert(0, nodeId+"~");
							
							Message msg = new Message(Message.NO_FORWARD, myId, Message.MESSAGE_REQUEST, buffer.toString());
							
							if ( log.isDebugEnabled() )
								log.debug("ask for missing msg: " + buffer.toString());
							
							if ( StatusManager.getInstance().isRoot() )
								CommsManager.getInstance().forwardDown(msg);
							else
								CommsManager.getInstance().forwardUp(msg);
						}
					}
				}
			}
		}
	}
	
	private void askUnknownStateMsg(String sourceId, String states)
	{
		StringTokenizer statesToken = new StringTokenizer(states, "|");
		while ( statesToken.hasMoreTokens() )
		{
			StringTokenizer tokens = new StringTokenizer(statesToken.nextToken(), "~");
			String nodeId = tokens.nextToken();
			int msgLocalSeqNo = Integer.parseInt(tokens.nextToken());
			
			if ( !nodeId.equals(sourceId) && !nodeId.equals(myId) )
			{
				if ( getState(nodeId) == 0 )
				{
					//ask from parent
					String stateMsgReq = nodeId + "~" + msgLocalSeqNo;
					
					if ( log.isDebugEnabled() )
						log.debug("Asking unknown state msg of: " + stateMsgReq);
					
					Message msg = new Message(Message.NO_FORWARD, myId, Message.MESSAGE_REQUEST, stateMsgReq);
					if ( StatusManager.getInstance().isRoot() )
						CommsManager.getInstance().forwardDown(msg);
					else
						CommsManager.getInstance().forwardUp(msg);
				}
			}
		}
	}
	
	private void addMsgToOutOfSeq(Message msg)
	{
		Map<Integer, String> map = msgOutOfSeq.get(msg.getSourceId());
		if ( map == null ) {
			map = Collections.synchronizedMap(new LinkedHashMap<Integer, String>());
			msgOutOfSeq.put(msg.getSourceId(), map);
		}
		map.put(msg.getLocalSeqNo(), msg.toString());
	}
	
	private void removeMsgFromOutOfSeq(Message msg)
	{
		Map<Integer, String> map = msgOutOfSeq.get(msg.getSourceId());
		if ( map != null )
			map.remove(msg.getLocalSeqNo());
	}
	
	private boolean isMsgInOutOfSeq(String nodeId, int msgLocalSeqNo)
	{
		Map<Integer, String> map = msgOutOfSeq.get(nodeId);
		if ( map == null )
			return false;
		
		if ( map.get(msgLocalSeqNo) == null )
			return false;
		return true;
	}
	
	private void acceptMsg(Message msg)
	{
		Map<Integer, String> map = msgAccepted.get(msg.getSourceId());
		if ( map == null ) {
			map = Collections.synchronizedMap(new MsgHistory<Integer, String>());
			msgAccepted.put(msg.getSourceId(), map);
		}
		map.put(msg.getLocalSeqNo(), msg.toString());
		
		if ( log.isDebugEnabled() )
			log.debug("Message accepted, state: " + msg.getLocalSeqNo());
	}
	
	public Message getMessage(String nodeId, int msgLocalSeqNo)
	{
		Map<Integer, String> map = msgAccepted.get(nodeId);
		if ( map == null )
			return null;
		String contents = map.get(msgLocalSeqNo);
		if ( contents == null )
			return null;
		return Message.createMessage(contents);
	}
	
	synchronized public void reset()
	{
		stateVector.clear();
		msgAccepted.clear();
		msgOutOfSeq.clear();
		
		myId = AppConfig.getInstance().getMe().getId();
		addState(myId, 0);
	}
	
	synchronized public void stampOwnMessage(Message msg)
	{
		int newState = getState(myId) + 1;
		addState(myId, newState);
		msg.setLocalSeqNo(newState);
		msg.setStates(this.getStates());
		acceptMsg(msg);
	}
	
	synchronized private void addState(String nodeId, int msgLocalSeqNo)
	{
		stateVector.put(nodeId, msgLocalSeqNo);
		if ( log.isDebugEnabled() )
			log.debug("ID: " + nodeId + ", new state: " + msgLocalSeqNo);
	}
	
	private int getState(String nodeId)
	{
		Integer msgLocalSeqNo = stateVector.get(nodeId);
		if ( msgLocalSeqNo == null )
			return 0;
		return msgLocalSeqNo;
	}
	
	private String getStates()
	{
		if ( stateVector.size() == 0 )
			return " ";
		
		StringBuffer buffer = new StringBuffer();
		Iterator<String> iter = stateVector.keySet().iterator();
		while ( iter.hasNext() )
		{
			String nodeId = iter.next();
			int msgLocalSeqNo = stateVector.get(nodeId);
			buffer.append(nodeId+"~"+msgLocalSeqNo+"|");
		}
		
		return buffer.toString();
	}
	
}//end of class CommsHistory
